package com.morphiastic;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;

import org.elasticsearch.common.xcontent.XContentBuilder;

import com.morphiastic.analysis.analyzers.ElasticAnalyzer;
import com.morphiastic.annotations.Analyze;
import com.morphiastic.annotations.ElastiField;
import com.morphiastic.annotations.ElastiFieldEmbedded;
import com.morphiastic.annotations.ElastiFieldType;
import com.morphiastic.annotations.ElastiMap;
import com.morphiastic.tests.entities.shape.Embedded1;
import com.morphiastic.utils.ReflectionTools;

import static org.elasticsearch.common.xcontent.XContentFactory.*;
import java.lang.reflect.AccessibleObject;


/**
 * 
 * 
 * TODO manage the case when the fields should be embedded ones
 * 
 * 
 * @author Alex
 *
 */
public class IndexedMapField implements IndexedField{

//	private Field field;
	private boolean isMap;
	private String fieldOriginalName;
	private String propertyName;
	
	/**
	 * regular types for the map
	 */
	private ElastiField keyAnnotation;
	private ElastiField valueAnnotation; 
	
	/**
	 * Embedded types for the map when the types contained in the 
	 * map should be embedded in the index object
	 */
	private ElastiFieldEmbedded embeddedKeyAnnotation;
	private ElastiFieldEmbedded embeddedValueAnnotation;
	
//	private StandardIndexedField standardKeyField;
//	private StandardIndexedField standardValueField;
//	
//	private IndexedEmbeddedField embeddedKeyField;
//	private IndexedEmbeddedField embeddedValueField;
	
	boolean keyFieldIsEmbedded;
	boolean valueFieldIsEmbedded;
	
	private IndexedField keyField;
	private IndexedField valueField;
	
	private ElasticStore es;
	
	public IndexedMapField(Field field, ElasticStore es){
		this(field, null,null,es);
	}
	
	public IndexedMapField(Method method,ElasticStore es){
		this(method,null,null,es);
	}
	
	public IndexedMapField(Method method, HashMap<AccessibleObject, Integer> known, Integer maxCircularDepth,ElasticStore es){
		this(method.getAnnotation(ElastiMap.class), method.getReturnType(), method.getGenericReturnType(), method.getName(), known, maxCircularDepth ,es);
	}
	
	public IndexedMapField(Field field,  HashMap<AccessibleObject, Integer> known, Integer maxCircularDepth,ElasticStore es){
		this(field.getAnnotation(ElastiMap.class), field.getType(), field.getGenericType(), field.getName(), known,maxCircularDepth,es);
	}
	
	private IndexedMapField(ElastiMap fan, Class<?> fieldType, Type genericType, String originalName, HashMap<AccessibleObject, Integer> known, Integer maxCircularDepth,ElasticStore es){
		if(fan==null)throw new IllegalArgumentException("The field was not annotated with the propper ElastiMap annotation");
		this.es = es;
		isMap = Map.class.isAssignableFrom(fieldType);
		if(!isMap)throw new IllegalArgumentException("The field is Not a Map but was annotated with the ElastiMap annotation"); 
		this.fieldOriginalName = originalName;	
		this.propertyName = fan.name();
		if(this.propertyName.isEmpty())this.propertyName = originalName;
	
		this.keyAnnotation = fan.keyAnnotation();
		this.valueAnnotation = fan.valueAnnotation();
		
		ElastiFieldEmbedded embeddedKeyAnnotation = fan.embeddedKeyAnnotation();
		ElastiFieldEmbedded embeddedValueAnnotation = fan.embeddedValueAnnotation();
	
	
		
		

		
		//figure out the types of both key and value
		Class<?> keyType = getParametrizedActualType(genericType, 0);
		
		//check if we have some embedded annotation for key and value
		if(embeddedKeyAnnotation != null && embeddedKeyAnnotation.circularRefMaxDepth()!=-1){
			this.keyAnnotation = null;
			this.embeddedKeyAnnotation = embeddedKeyAnnotation;
			IndexedEmbeddedField indexationRule = null;
			if(keyType.isArray()){
				indexationRule = new IndexedEmbeddedField(keyType.getComponentType(), "key", embeddedKeyAnnotation, known, this.embeddedKeyAnnotation.circularRefMaxDepth(),false,es);
			}else if(Collection.class.isAssignableFrom(keyType)){
//				Type genericType = field.getGenericType();
				 if (genericType instanceof ParameterizedType) {  
			            ParameterizedType pt = (ParameterizedType) genericType;  
			            Type t1 =  pt.getActualTypeArguments()[0];
			            ParameterizedType pt2 = (ParameterizedType) t1; 
			            Class<?> actualType  =  (Class<?>) pt2.getActualTypeArguments()[0];
			            indexationRule = new IndexedEmbeddedField(actualType, "key", embeddedKeyAnnotation, known, this.embeddedKeyAnnotation.circularRefMaxDepth(),false,es);
			      }  
			}else{
				indexationRule = new IndexedEmbeddedField( keyType,  "key", this.embeddedKeyAnnotation, known, this.embeddedKeyAnnotation.circularRefMaxDepth(),false ,es);

			}
			this.keyField = indexationRule;
			this.keyFieldIsEmbedded = true;
		}else{
			this.keyField = new StandardIndexedField(this.keyAnnotation, keyType, keyType.getGenericSuperclass(), "key",es);
			this.keyFieldIsEmbedded = false;
		} 
		
		
		
		Class<?> valueType = getParametrizedActualType(genericType, 1);
		//check if we have some embedded annotation for     value
		if(embeddedValueAnnotation != null && embeddedValueAnnotation.circularRefMaxDepth()!=-1){
			this.valueAnnotation = null;
			this.embeddedValueAnnotation = embeddedValueAnnotation;
			IndexedEmbeddedField indexationRule = null;
			if(valueType.isArray()){
				indexationRule = new IndexedEmbeddedField(valueType.getComponentType(), "value", embeddedValueAnnotation, known, this.embeddedValueAnnotation.circularRefMaxDepth(),false,es);
			}else if(Collection.class.isAssignableFrom(valueType)){
//				Type genericType = field.getGenericType();
				 if (genericType instanceof ParameterizedType) {  
			            ParameterizedType pt = (ParameterizedType) genericType;  
			            Type t1 =  pt.getActualTypeArguments()[1];
			            ParameterizedType pt2 = (ParameterizedType) t1; 
			            Class<?> actualType  =  (Class<?>) pt2.getActualTypeArguments()[0];
			            indexationRule = new IndexedEmbeddedField(actualType, "value", embeddedValueAnnotation, known, this.embeddedValueAnnotation.circularRefMaxDepth(),false,es);
			      }  
			}else{
				indexationRule = new IndexedEmbeddedField( valueType,  "value", this.embeddedValueAnnotation, known, this.embeddedValueAnnotation.circularRefMaxDepth() ,false,es);
			}
			this.valueField = indexationRule;
			this.valueFieldIsEmbedded = true;
		}else{
			this.valueField = new StandardIndexedField(this.valueAnnotation, valueType, valueType.getGenericSuperclass(), "value",es);
			this.valueFieldIsEmbedded = false;
		}
	}
	
	
	
	
	
	/**
	 *  Method used to set the field parameters in the ES index for this field
	 * @param mappingBuilder
	 * @throws IOException 
	 */
	public void addIndexFieldSettings(XContentBuilder mappingBuilder) throws IOException {
		mappingBuilder.startObject(this.fieldOriginalName);
//			mappingBuilder.field("index_name", this.propertyName); //commented out on 09.06.12 see below
			if(!this.fieldOriginalName.equals(this.propertyName))mappingBuilder.field("index_name", this.propertyName); // only add the field name if its different from the one of the actual field
//			mappingBuilder.field("type", "object");//should be object but removed as of 09.06.12 because its the default
			mappingBuilder.startObject("properties");//props of a map a key and value
//			if(standardKeyField == null && embeddedKeyField == null)throw new IllegalArgumentException("keyField == null && embeddedKeyField == null ... I cant createa mapping for this");
//			if(standardValueField == null && embeddedValueField == null)throw new IllegalArgumentException("valueField == null && embeddedValueField == null ... I cant createa mapping for this");
//			if(!keyFieldIsEmbedded){
				keyField.addIndexFieldSettings(mappingBuilder);
//			}else{
//				embeddedKeyField.addIndexFieldSettings(mappingBuilder);
//			}
//			if(standardValueField!=null){
				valueField.addIndexFieldSettings(mappingBuilder);
//			}else{
//				embeddedValueField.addIndexFieldSettings(mappingBuilder);
//			}
			mappingBuilder.endObject();
		mappingBuilder.endObject();
	}
	
	
	
	
	
	public void addFieldSettingsMap(HashMap<String, Object> props){
 
			//build the standard/default mapping as a map
			Map<String,Object> m = new HashMap<String, Object>();
			m.put("type", "object");
			if(!this.fieldOriginalName.equals(this.propertyName))m.put("index_name", this.propertyName);
		 
			HashMap<String,Object> propsMap = new HashMap<String, Object>();
			keyField.addFieldSettingsMap(propsMap);
			valueField.addFieldSettingsMap(propsMap);
			
			m.put("properties", propsMap);
			props.put(this.fieldOriginalName, m);
	}
	 
	
	
	
	
	
	
	
	
	
	
	
	public Set<ElasticAnalyzer> getAnalyzers(){
		Set<ElasticAnalyzer> allAnalyzerDefs = new HashSet<ElasticAnalyzer>();
		
		allAnalyzerDefs.addAll(keyField.getAnalyzers());
		allAnalyzerDefs.addAll(valueField.getAnalyzers());
		
		//old ugly shit
//		if(standardKeyField!=null){
//			allAnalyzerDefs.add(standardKeyField.getAnalyzer());
//			if(standardKeyField.getSearchAnalyzer()!=null)allAnalyzerDefs.add(standardKeyField.getSearchAnalyzer());
//		}
//		if(embeddedKeyField!=null)allAnalyzerDefs.addAll(embeddedKeyField.getAnalyzers());
//		if(standardValueField!=null){
//			allAnalyzerDefs.add(standardValueField.getAnalyzer());
//			if(standardValueField.getSearchAnalyzer()!=null)allAnalyzerDefs.add(standardValueField.getSearchAnalyzer());
//		}
//		if(embeddedValueField!=null)allAnalyzerDefs.addAll(embeddedValueField.getAnalyzers());

		return allAnalyzerDefs;
	}
	
	
	private  Class<?> getParametrizedActualType(Type genericType, int pos){
		if (genericType instanceof ParameterizedType) {  
            ParameterizedType pt = (ParameterizedType) genericType;  
            Type actualType = pt.getActualTypeArguments()[pos];
            if(actualType instanceof GenericArrayType ){
            	GenericArrayType gat = (GenericArrayType) actualType;
            	Class<?>  c = (Class<?>) gat.getGenericComponentType();
            	return c;
            }else if(actualType instanceof ParameterizedType){
            	//we are dealing with an arraylist or an array 
            	ParameterizedType pt2 = (ParameterizedType) actualType;  
            	Type actualType2 = pt2.getActualTypeArguments()[0];
	        	 Class<?>  c = (Class<?>) actualType2 ;
	             return c;
            }else{
            	 Class<?>  c = (Class<?>) actualType ;
                 return c;
            }
      }  
		throw new IllegalArgumentException("getParametrizedType genericType NOT instanceof ParameterizedType ");
	}
	
	 

	
	
	
	
	
	@Override
	public XContentBuilder writeIndexedField(XContentBuilder mappingBuilder ,  Object o, ElasticStore es,  Integer maxCircularRefDepth, IdentityHashMap<AccessibleObject, Integer> known) throws IOException {
		if(o==null){
			String nullStr = null;
			mappingBuilder.field(this.fieldOriginalName, nullStr);
			return mappingBuilder;
		}
		try {
			Field  field = ReflectionTools.getDeclaredFieldFromHierarchy(o.getClass(), fieldOriginalName); //o.getClass().getField(this.fieldOriginalName);
			field.setAccessible(true);
			Map<Object, Object> fieldValue = (Map)field.get(o);
			if(fieldValue==null){
				String nullStr = null;
				mappingBuilder.field(this.fieldOriginalName, nullStr);
				return mappingBuilder;
			}else{
				 mappingBuilder.startArray(this.fieldOriginalName);
//				 	mappingBuilder.startArray();
						for (Map.Entry<?, ?>  mapFieldEntry: fieldValue.entrySet()) {
								mappingBuilder.startObject();
								keyField.writeIndexedField(mappingBuilder, mapFieldEntry, es, maxCircularRefDepth, known);  
								valueField.writeIndexedField(mappingBuilder, mapFieldEntry, es, maxCircularRefDepth, known);
//									keyField.writeIndexedField(mappingBuilder, mapFieldEntry.getKey(), es, maxCircularRefDepth, known);  //TODO check if this is correct .... 
//									valueField.writeIndexedField(mappingBuilder, mapFieldEntry.getValue(), es, maxCircularRefDepth, known);
								 mappingBuilder.endObject();
						}
					mappingBuilder.endArray();
//				mappingBuilder.endObject();
			}
		} catch (SecurityException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchFieldException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return mappingBuilder;
	}
	
	
	
	
	
	
	
	
	
	
}
